package com.qcdp.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

//redis的工具类
@Component
public class RedisUtil {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //创建线程池
    private static final ExecutorService CACHE_THREAD_POOL= Executors.newFixedThreadPool(10);


    //方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间

    /**
     *
     * @param key  键
     * @param value java对象
     * @param timeOut 过期时间
     * @param timeUnit 过期的时间单位
     */
    public void  set(String key, Object value, Long timeOut, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key,  JSONUtil.toJsonStr(value),timeOut,timeUnit);
    }
    //方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题

    /**
     *
     * @param key 键
     * @param data 值
     * @param timeOut 过期时间
     * @param timeUnit 时间单位
     */
    public void setLogicRedisData(String key,Object data,Long timeOut,TimeUnit timeUnit){
        RedisData redisData = new RedisData();
        redisData.setData(data);
        //向后加多少秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeOut)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));

    }

    //方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题(泛型编程)
    /**
     *
     * @param keyPrefix  是我们key的前缀
     * @param id 我们数据的id
     * @param type 返回值的类型
     * @param dbFallBack 我们需要调用数据库查询的代码
     * @param timeOut 数据的超时时间
     * @param timeUnit 数据的过期时间
     * @param <R> 返回值的类型 用户的type是什么我们的返回值就是什么
     * @param <ID> id类型，用户传递的id是什么类型那么ID就是什么类型
     * @return
     */
    public <R , ID >  R queryWithCachePenetration(String keyPrefix , ID id, Class<R> type, Function<ID,R> dbFallBack
        ,Long timeOut ,TimeUnit timeUnit
    ){

        String cacheJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);

        //2.命中直接返回商铺信息，但是这里面要排除我们之前放的空对象
        if(StrUtil.isNotBlank(cacheJson)){
            return JSONUtil.toBean(cacheJson, type);
        }
        //3.判断缓存是否为空值
        if(cacheJson != null ){
            return  null;
        }
        //4.未命中，根据ID来查询数据库(谁调用我谁传递给我)
        //Shop shop = this.getById(id);
        R r = dbFallBack.apply(id);
        //5.判断商铺是否存在
        if(r == null){
            //5.1商铺不存在,我们缓存空对象，并且保持两分钟的有效期，防止短期不一致
            stringRedisTemplate.opsForValue().set(keyPrefix+id,
                    "",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return  null;
        }
        //5.2存在
        String jsonStr = JSONUtil.toJsonStr(r);
        //添加redis缓存并且设置有效期为半个小时
        stringRedisTemplate.opsForValue().set(keyPrefix+id, jsonStr, timeOut, timeUnit);

        return  r;
    }
    //方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
    public <R,ID> R queryWithLogicTimeOut(String keyPrefix ,ID id,Class<R> type,Function<ID,R> dbFallBack
            ,Long timeOut ,TimeUnit timeUnit
    ){
        //1.查询redis
        String cacheJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.未命中。你访问不是我们热点key，缓存里面没有数据库当中肯定也没有
        if(StrUtil.isBlank(cacheJson)){
            return null;
        }
        //3.命中，判断缓存是否过期
        RedisData redisData = JSONUtil.toBean(cacheJson, RedisData.class);
        //获取逻辑过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject dataJson = (JSONObject)redisData.getData();
        R r = JSONUtil.toBean(dataJson, type);
        //4.未过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //没有过期，直接返回
            return  r;
        }
        //5.过期，尝试获取互斥锁
        Boolean tryLock = tryLock(id);
        //6.获取锁,我要开启独立的线程
        if(tryLock){
            CACHE_THREAD_POOL.submit(()->{
                //开启新的的线程，根据ID来查询店铺的信息
                try {
                    R r1 = dbFallBack.apply(id);
                    //把我们店铺信息重新写入到我们redis里面并且更新逻辑过期时间
                    //重建缓存的时候我们搞一些延迟,方便我们测试
                    Thread.sleep(50);
                    this.setLogicRedisData(keyPrefix+id,r1,timeOut,timeUnit);
                } catch (Exception e) {
                    throw  new RuntimeException();
                } finally {
                    //无论我们的代码出现了什么问题，我这锁一定要释放
                    unLock(id);
                }
            });
        }
        //我拿到锁了开新的线程去更新缓存，我返回旧的数据，我没有拿到锁直接返回旧的数据
        return r;
    }


    //加锁
    public <ID> Boolean tryLock(ID id){
        //setnx: 返回值为true说明我的值存放成功了（获取到锁了），反之就存放失败了（有人在做这个动作）.需要设置超时时间来作为兜底方案
        Boolean falg = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1"
                ,RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return falg;
    }

    //解锁
    public <ID> void unLock(ID id){
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY +id);
    }
}
